一 本文目的
- 学习 HTTP2 协议.
- 学习 NGINX 对 HTTP2 的处理以及如何从 HTTP2 进入 HTTP1.1 的处理流程.
- 未分析 HEADERS、DATA 之外帧处理.
二 协议概述
HTTP2 可以同时运行在 HTTP/HTTPS 之上, 在 HTTPS 上是通过 TLS 应用层协商协议(Application-Layer Protocol Negotiation 简称 ALPN)支持. NGINX 服务器是支持在 HTTP 下开启 HTTP2, 但是无法在同一个端口上同时支持 HTTP2、HTTP. 浏览器厂商选择只实现基于 HTTPS 的 HTTP2, 使用 ALPN 可以判断使用 HTTP/HTTP2.
HTTP2 协议特性:
二进制分帧协议
无队头堵塞: 可以并行发送多个 HTTP 请求, 不会触发队头阻塞.
多路复用
二进制流可以交错进行, 实现在单 TCP 连接上多路复用. 同时连接复用有效避免 TCP 慢启动、拥塞窗口协商过程, 提升传输效率. 可以将 Stream 想象为一系列 Frame 序列. 参见 Streams And Multiplexing.
服务端推送
头部压缩
头部压缩是有状态的, 在一个连接上只有一个压缩、解压上下文.
NGINX 默认没有编译 HTTP2 模块, 需要通过
--with-http_v2_module
选项开启.协议细节参考 RFC 文档. 在规范中未定义 Frame ID 类似字段, 是通过 TCP 协议来确保同一个 Stream 中的 Frame 是有序的.
1. 交互顺序
1 | sequenceDiagram |
协议规定, 对端响应的 SETTINGS 帧是前言的一部分, 因此 Server 必须发送 SETTINGS 帧.
对基于 HTTPS 的 HTTP2 在 SSL 握手阶段需要进行应用层协议协商(ALPN), 当 Sever 允许使用 HTTP2 时会响应 101 状态码进行协议切换, 会产生一次额外的 RTT.
Stream ID 约束: 客户端使用奇数流标识符; 服务端使用偶数流标识符. 特殊的
0
用于整个连接而非单独的流. 可以观察到 SETTINGS 帧的 Stream ID 都是0
.
流中可以有多个帧, 会出现多个帧具有相同的 Stream ID, 这些帧不会错乱是因为: 1. 发送方在同一个流中顺序发送; 2. TCP 协议确保帧会按发送顺序交付.
三 NGINX 中处理
对于 H2 处理有个比较重要的问题, 如何对一个 TCP 链接进行多路复用. NGINX 使用 fake_connection 与 H2 的 Stream 关联, 将其等价于 HTTP1.1 的 request 复用原有模块.
1. H2 入口
NGINX 中 HTTP2 协议处理有两个入口: 基于 HTTP 和基于 HTTPS.
当配置
http2
指令, 未开启 HTTPS 时, 会在ngx_http_init_connection
阶段修改当前连接的接收处理函数为ngx_http_v2_init
陷入 HTTP2 协议处理流程.当开启 HTTPS 时, 会在 SSL 握手阶段, 根据请求协商信息确定是否启用 HTTP2.
2. H2 处理开始
NGINX 中 HTTP2 协议是由 ‘ngx_http_v2.c’ 模块实现.
在与客户端交互过程中, NGINX 会首先发送一个 SETTINGS 和 WINDOW_UPDATE 帧, 通知客户端 NGINX 支持的最大流数量、窗口大小、帧大小, 此时帧会先进行 queue 合并发送. TCP 连接的读/写事件处理函数为 ngx_http_v2_read_handler
, ngx_http_v2_write_handler
, HTTP2 状态机初始状态是 ngx_http_v2_state_preface
.
ngx_http_v2_state_preface
检查发送“前言”是否正确, “前言”必须是 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
. 在“前言”处理完毕后会进入请求头处理 ngx_http_v2_state_head
, 此时会根据帧首部 Type
使用不同的处理函数处理. 不同类型帧对应处理函数:
1 | static ngx_http_v2_handler_pt ngx_http_v2_frame_states[] = { |
3. 请求头处理
H2 同样遵循 HTTP1.1 的语义, 需要先发送请求行、请求头, 不过在 H2 中将请求行信息转换成特殊的请求头. 对于 HTTP1.1 请求行是 Method SP Request-URI SP HTTP-Version CRLF
格式, 在 H2 中会将其拆分成 :method
, :path
请求头.
请求头是通过 ngx_http_v2_state_headers
, ngx_http_v2_state_header_block
, ngx_http_v2_state_field_len(ngx_http_v2_state_field_huff/ngx_http_v2_state_field_raw)
, ngx_http_v2_state_process_header
, ngx_http_v2_state_header_complete
函数处理.
ngx_http_v2_state_headers
判断 StreamId 是否正确、请求头是否超限、流并发是否超限、分配 Stream 结构建立流依赖树.
ngx_http_v2_state_header_block
请求头处理, 有 5 种类型的请求头(具体看 HPACK 协议, 不展开). 根据请求头长度进行处理.
ngx_http_v2_state_field_len(ngx_http_v2_state_field_huff/ngx_http_v2_state_field_raw)
使用霍夫曼编码或原始编码解析请求头.
ngx_http_v2_state_process_header
对解析出来的请求头进行处理: 校验是否合法, 将其保存到
r->headers_in.headers
中.ngx_http_v2_state_header_complete
判断是否有后续请求头, 进入
ngx_http_v2_state_header_block
继续处理. 如果设置标记 HEADERS 帧结束, 进入请求处理.
4. 请求处理
在 H2 请求头处理结束后会进入 ngx_http_v2_run_request
进行请求处理:
- 将 H2 格式请求信息转换成 HTTP1 格式信息(NGINX 内部使用, 能够复用 NGINX 原有功能);
- 进入 HTTP1 的请求处理函数
ngx_http_process_request
, 调用ngx_http_handler
运行 HTTP 处理的 11 个阶段.
在调用
ngx_http_v2_run_request
时使用的是stream->request
作为参数, 是在ngx_http_v2_state_headers
函数中创建的假的request/connection
, 读/写回调函数都是ngx_http_v2_close_stream_handler
.
在
ngx_http_process_request
中将请求读回调函数修改为ngx_http_block_reading
.
在
ngx_http_handler
中将请求写回调函数修改为ngx_http_core_run_phases
.
在
ngx_http_v2_read_request_body
中将读/写回调函数修改为ngx_http_v2_read_client_request_body_handler/ngx_http_request_empty_handler
.
5. 何时读取请求体?
前面已经提到在 ngx_http_handler
中会运行 HTTP 处理的 11 个阶段, 假设当前 location 用于反向代理那必定会运行 ngx_http_proxy_handler
函数. 可以追踪到函数调用链(注意, ngx_http_v2_read_client_request_body_handler
是通过回调触发):
1 | graph LR |
看到这里是不是会想 ngx_http_v2_read_client_request_body_handler
负责请求体读取操作操作? 跟进函数去没有读取操作, 而且函数参数 r->connection
并没有与 socket 关联无法进行读写操作.
对于 H2 数据是通过 DATA 帧进行传输, 还是得从 ngx_http_v2_frame_states
状态机跟进. ngx_http_v2_state_data
用于处理 DATA 帧, 其中调用链如下:
1 | graph LR |
此处 post_handler
就是 ngx_http_upstream_init
, 是 ngx_http_proxy_handler
中设置.
6. 响应
NGINX 响应分为 HEADER、BODY 两个阶段, 看下源码有 ngx_http_v2_filter_module
模块, 模块只介入 header_filter
处理阶段, 在其中以 H2 格式发送响应头, 有两点需要注意:
连接(
connection
)以及对应的套接字(socket
):对于 H2 在
header_filter/body_filter
的入参request
是“假”的, 其关联的connection
也是假的. 需要使用 H2 初始建立的连接, 通过r->stream->connection
索引.响应体处理:
H2 模块没有添加
body_filter
处理函数, 在header_filter
阶段修改connection
的send_chain
回调函数为ngx_http_v2_send_chain
用于 H2 响应体发送.响应 Stream Id:
在响应阶段
request
是“假”的, 已经与请求 Stream 关联, 通过r->stream->node->id
可以获得sid
.
这里只做了简略分析, 优先级、推送、窗口更新都没有提及.
四 抓包观察
1. 配置
NGINX 可以不基于 HTTPS 启用 H2, 配置如下:
1 | server { |
2. 请求
可以使用 CURL 发起 H2 请求:
1 | curl -i --http2-prior-knowledge http://127.0.0.1:8000/ |
CURL 也支持发起 H3 请求, 方便抓包测试.
3. 抓包
客户端发送的 SETTINGS 帧信息
服务端发送的 SETTINGS 帧信息
注意, 服务端在响应 SETTINGS、WINDOW_UPDATE 帧时 Stream ID 都为 0
; 在发送 HEADERS 帧 Stream ID 为 1
, 是对客户端发起的 Stream ID 为 1
的响应. 即请求/响应在同一个流中进行.